/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */
package org.opensolaris.os.dtrace;

import java.io.*;
import java.beans.*;
import java.util.*;

/**
 * A record generated by the DTrace {@code printa()} action.  Lists the
 * aggregations passed to {@code printa()} and records the formatted
 * output associated with each {@link Tuple}.  If multiple aggregations
 * were passed to the {@code printa()} action that generated this
 * record, then the DTrace library tabulates the output, using a default
 * format if no format string was specified.  By default, the output
 * string associated with a given {@code Tuple} includes a value from
 * each aggregation, or zero wherever an aggregation has no value
 * associated with that {@code Tuple}.  For example, the D statements
 * <pre><code>
 *     &#64;a[123] = sum(1);
 *     &#64;b[456] = sum(2);
 *     printa(&#64;a, &#64;b, &#64;c);
 * </code></pre>
 * produce output for the tuples "123" and "456" similar to the
 * following:
 * <pre><code>
 *	123	1	0	0
 *	456	0	2	0
 * </code></pre>
 * The first column after the tuple contains values from {@code @a},
 * the next column contains values from {@code @b}, and the last
 * column contains zeros because {@code @c} has neither a value
 * associated with "123" nor a value associated with "456".
 * <p>
 * If a format string is passed to {@code printa()}, it may limit the
 * aggregation data available in this record.  For example, if the
 * format string specifies a value placeholder for only one of two
 * aggregations passed to {@code printa()}, then the resulting {@code
 * PrintaRecord} will contain only one {@code Aggregation}.  If no value
 * placeholder is specified, or if the aggregation tuple is not
 * completely specified, the resulting {@code PrintaRecord} will contain
 * no aggregation data.  However, the formatted output generated by the
 * DTrace library is available in all cases.  For details about
 * {@code printa()} format strings, see the <a
 * href=http://dtrace.org/guide/chp-fmt.html#chp-fmt-printa>
 * <b>{@code printa()}</b></a> section of the <b>Output
 * Formatting</b> chapter of the <i>Dynamic Tracing Guide</i>.
 * <p>
 * Immutable.  Supports persistence using {@link java.beans.XMLEncoder}.
 *
 * @author Tom Erickson
 */
public final class PrintaRecord implements Record, Serializable,
	Comparable <PrintaRecord> {
    static final long serialVersionUID = -4174277639915895694L;

    static {
	try {
	    BeanInfo info = Introspector.getBeanInfo(PrintaRecord.class);
	    PersistenceDelegate persistenceDelegate =
		    new DefaultPersistenceDelegate(
		    new String[] {"snaptime", "aggregations",
		    "formattedStrings", "tuples", "output"})
	    {
		/*
		 * Need to prevent DefaultPersistenceDelegate from using
		 * overridden equals() method, resulting in a
		 * StackOverFlowError.  Revert to PersistenceDelegate
		 * implementation.  See
		 * http://forum.java.sun.com/thread.jspa?threadID=
		 * 477019&tstart=135
		 */
		protected boolean
		mutatesTo(Object oldInstance, Object newInstance)
		{
		    return (newInstance != null && oldInstance != null &&
			    oldInstance.getClass() == newInstance.getClass());
		}
	    };
	    BeanDescriptor d = info.getBeanDescriptor();
	    d.setValue("persistenceDelegate", persistenceDelegate);
	} catch (IntrospectionException e) {
	    System.out.println(e);
	}
    }

    /** @serial */
    private final long snaptime;
    /** @serial */
    private List <Aggregation> aggregations;
    /** @serial */
    private Map <Tuple, String> formattedStrings;
    /** @serial */
    private List <Tuple> tuples;
    private transient StringBuilder outputBuffer;
    private transient String output;
    private transient boolean formatted;

    /**
     * Package level access, called by ProbeData.
     */
    PrintaRecord(long snaptimeNanos, boolean isFormatString)
    {
	snaptime = snaptimeNanos;
	aggregations = new ArrayList <Aggregation> ();
	formattedStrings = new HashMap <Tuple, String> ();
	tuples = new ArrayList <Tuple> ();
	outputBuffer = new StringBuilder();
	formatted = isFormatString;
	validate();
    }

    /**
     * Creates a record with the given snaptime, aggregations, and
     * formatted output.
     *
     * @param snaptimeNanos  nanosecond timestamp of the snapshot used
     * to create this {@code printa()} record
     * @param aggs  aggregations passed to the {@code printa()} action
     * that generated this record
     * @param formattedOutput  the formatted output, if any, associated
     * with each {@code Tuple} occurring in the aggregations belonging
     * to this record, one formatted string per {@code Tuple}, or an
     * empty or {@code null} map if an incomplete {@code printa()}
     * format string caused aggregation tuples to be omitted from this
     * record
     * @param orderedTuples list of aggregation tuples in the same order
     * generated by the native DTrace library (determined by the various
     * "aggsort" options such as {@link Option#aggsortkey})
     * @param formattedOutputString {@code printa()} formatted string
     * output in the same order generated by the native DTrace library
     * (determined by the various "aggsort" options such as
     * {@link Option#aggsortkey})
     * @throws NullPointerException if the given collection of
     * aggregations is {@code null}, or if the given ordered lists of
     * tuples or formatted strings are {@code null}
     * @throws IllegalArgumentException if the given snaptime is
     * negative
     */
    public
    PrintaRecord(long snaptimeNanos, Collection <Aggregation> aggs,
	    Map <Tuple, String> formattedOutput,
	    List <Tuple> orderedTuples,
	    String formattedOutputString)
    {
	snaptime = snaptimeNanos;
	if (aggs != null) {
	    aggregations = new ArrayList <Aggregation> (aggs.size());
	    aggregations.addAll(aggs);
	}
	if (formattedOutput != null) {
	    formattedStrings = new HashMap <Tuple, String>
		    (formattedOutput);
	}
	if (orderedTuples != null) {
	    tuples = new ArrayList <Tuple> (orderedTuples.size());
	    tuples.addAll(orderedTuples);
	}
	output = formattedOutputString;
	validate();
    }

    private final void
    validate()
    {
	if (snaptime < 0) {
	    throw new IllegalArgumentException("snaptime is negative");
	}
	if (aggregations == null) {
	    throw new NullPointerException("aggregations list is null");
	}
	Aggregation a;
	for (int i = 0, len = aggregations.size(); i < len; ++i) {
	    a = aggregations.get(i);
	    if (a == null) {
		throw new NullPointerException(
			"null aggregation at index " + i);
	    }
	}
	if (tuples == null) {
	    throw new NullPointerException("ordered tuple list is null");
	}
	if (output == null && outputBuffer == null) {
	    throw new NullPointerException("formatted output is null");
	}
    }

    /**
     * Gets the nanosecond timestamp of the aggregate snapshot used to
     * create this {@code printa()} record.
     *
     * @return nanosecond timestamp
     */
    public long
    getSnaptime()
    {
	return snaptime;
    }

    private Aggregation
    getAggregationImpl(String name)
    {
	if (name == null) {
	    return null;
	}
	for (Aggregation a : aggregations) {
	    if (name.equals(a.getName())) {
		return a;
	    }
	}
	return null;
    }

    /**
     * Gets the named aggregation.
     *
     * @return the named aggregation passed to {@code printa()}, or
     * {@code null} if the named aggregation is not passed to {@code
     * printa()}, or if it is omitted due to an incomplete {@code
     * printa()} format string, or if it is empty (a future release of
     * this API may represent an empty DTrace aggregation as a non-null
     * {@code Aggregation} with no records; users of this API should not
     * rely on a non-null return value to indicate a non-zero record
     * count)
     */
    public Aggregation
    getAggregation(String name)
    {
	name = Aggregate.filterUnnamedAggregationName(name);
	return getAggregationImpl(name);
    }

    /**
     * Gets a list of the aggregations passed to the {@code printa()}
     * action that generated this record.  The returned list is a copy,
     * and modifying it has no effect on this record.  Supports XML
     * persistence.
     *
     * @return non-null, possibly empty list of aggregations belonging
     * to this record (empty aggregations are excluded)
     */
    public List <Aggregation>
    getAggregations()
    {
	return new ArrayList <Aggregation> (aggregations);
    }

    /**
     * Gets the formatted string, if any, associated with the given
     * aggregation tuple.
     *
     * @param key aggregation tuple
     * @return the formatted string associated with the given
     * aggregation tuple, or {@code null} if the given tuple does not
     * exist in the aggregations belonging to this record or if it
     * is omitted from this record due to an incomplete {@code printa()}
     * format string
     * @see #getFormattedStrings()
     * @see #getOutput()
     */
    public String
    getFormattedString(Tuple key)
    {
	if (formattedStrings == null) {
	    return null;
	}
	return formattedStrings.get(key);
    }

    /**
     * Gets the formatted output, if any, associated with each {@code
     * Tuple} occurring in the aggregations belonging to this record,
     * one formatted string per {@code Tuple}.  Gets an empty map if
     * aggregation tuples are omitted from this record due to an
     * incomplete {@code printa()} format string.  The returned map is a
     * copy and modifying it has no effect on this record.  Supports XML
     * persistence.
     *
     * @return a map of aggregation tuples and their associated
     * formatted output strings, empty if aggregation tuples are omitted
     * from this record due to an incomplete {@code printa(}) format
     * string
     * @see #getFormattedString(Tuple key)
     * @see #getOutput()
     */
    public Map <Tuple, String>
    getFormattedStrings()
    {
	if (formattedStrings == null) {
	    return new HashMap <Tuple, String> ();
	}
	return new HashMap <Tuple, String> (formattedStrings);
    }

    /**
     * Gets an ordered list of this record's aggregation tuples.  The
     * returned list is a copy, and modifying it has no effect on this
     * record.  Supports XML persistence.
     *
     * @return a non-null list of this record's aggregation tuples in
     * the order they were generated by the native DTrace library, as
     * determined by the {@link Option#aggsortkey}, {@link
     * Option#aggsortrev}, {@link Option#aggsortpos}, and {@link
     * Option#aggsortkeypos} options
     */
    public List <Tuple>
    getTuples()
    {
	return new ArrayList <Tuple> (tuples);
    }

    /**
     * Gets this record's formatted output.  Supports XML persistence.
     *
     * @return non-null formatted output in the order generated by the
     * native DTrace library, as determined by the {@link
     * Option#aggsortkey}, {@link Option#aggsortrev}, {@link
     * Option#aggsortpos}, and {@link Option#aggsortkeypos} options
     */
    public String
    getOutput()
    {
	if (output == null) {
	    output = outputBuffer.toString();
	    outputBuffer = null;
	    if ((output.length() == 0) && !formatted) {
		output = "\n";
	    }
	}
	return output;
    }

    /**
     * Package level access, called by ProbeData.
     *
     * @throws NullPointerException if aggregationName is null
     * @throws IllegalStateException if this PrintaRecord has an
     * aggregation matching the given name and it already has an
     * AggregationRecord with the same tuple key as the given record.
     */
    void
    addRecord(String aggregationName, long aggid, AggregationRecord record)
    {
	if (formattedStrings == null) {
	    // printa() format string does not completely specify tuple
	    return;
	}

	aggregationName = Aggregate.filterUnnamedAggregationName(
		aggregationName);
	Aggregation aggregation = getAggregationImpl(aggregationName);
	if (aggregation == null) {
	    aggregation = new Aggregation(aggregationName, aggid);
	    aggregations.add(aggregation);
	}
	try {
	    aggregation.addRecord(record);
	} catch (IllegalArgumentException e) {
	    Map <Tuple, AggregationRecord> map = aggregation.asMap();
	    AggregationRecord r = map.get(record.getTuple());
	    //
	    // The printa() format string may specify the value of the
	    // aggregating action multiple times.  While that changes
	    // the resulting formatted string associated with the tuple,
	    // we ignore the attempt to add the redundant record to the
	    // aggregation.
	    //
	    if (!r.equals(record)) {
		throw e;
	    }
	}
    }

    //
    // Called from native code when the tuple is not completely
    // specified in the printa() format string.
    //
    void
    invalidate()
    {
	formattedStrings = null;
	aggregations.clear();
	tuples.clear();
    }

    void
    addFormattedString(Tuple tuple, String formattedString)
    {
	if (tuple != null && formattedStrings != null) {
	    if (formattedStrings.containsKey(tuple)) {
		throw new IllegalArgumentException("A formatted string " +
			"for tuple " + tuple + " already exists.");
	    } else {
		formattedStrings.put(tuple, formattedString);
		tuples.add(tuple);
	    }
	}
	outputBuffer.append(formattedString);
    }

    /**
     * Compares the specified object with this {@code PrintaRecord} for
     * equality. Returns {@code true} if and only if the specified
     * object is also a {@code PrintaRecord} and both records have the
     * same aggregations and the same formatted strings in the same
     * order (by aggregation tuple).
     *
     * @return {@code true} if and only if the specified object is also
     * a {@code PrintaRecord} and both records have the same
     * aggregations and the same formatted strings in the same order (by
     * aggregation tuple)
     */
    @Override
    public boolean
    equals(Object o)
    {
	if (o instanceof PrintaRecord) {
	    PrintaRecord r = (PrintaRecord)o;
	    return (aggregations.equals(r.aggregations) &&
		    ((formattedStrings == null || formattedStrings.isEmpty())
		    ? (r.formattedStrings == null ||
			r.formattedStrings.isEmpty())
		    : formattedStrings.equals(r.formattedStrings)) &&
		    tuples.equals(r.tuples));
	}

	return false;
    }

    /**
     * Overridden to ensure that equal instances have equal hash codes.
     */
    @Override
    public int
    hashCode()
    {
	int hash = 17;
	hash = (hash * 37) + aggregations.hashCode();
	hash = (hash * 37) + ((formattedStrings == null ||
	    formattedStrings.isEmpty()) ? 0 :
	    formattedStrings.hashCode());
	hash = (hash * 37) + tuples.hashCode();
	return hash;
    }

    /**
     * Compares the formatted {@link #getOutput() output} of this record
     * with that of the given record. Note that ordering {@code printa}
     * records by their output string values is incompatible with {@link
     * #equals(Object o) equals()}, which also checks the underlying
     * aggregation data for equality.
     *
     * @return a negative number, 0, or a positive number as this
     * record's formatted output is lexicographically less than, equal
     * to, or greater than the given record's formatted output
     */
    public int
    compareTo(PrintaRecord r)
    {
	return getOutput().compareTo(r.getOutput());
    }

    /**
     * Serialize this {@code PrintaRecord} instance.
     *
     * @serialData Serialized fields are emitted, followed by the
     * formatted output string.
     */
    private void
    writeObject(ObjectOutputStream s) throws IOException
    {
	s.defaultWriteObject();
	if (output == null) {
	    s.writeObject(outputBuffer.toString());
	} else {
	    s.writeObject(output);
	}
    }

    private void
    readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException
    {
	s.defaultReadObject();
	output = (String)s.readObject();
	// make defensive copy
	if (aggregations != null) {
	    List <Aggregation> copy = new ArrayList <Aggregation>
		    (aggregations.size());
	    copy.addAll(aggregations);
	    aggregations = copy;
	}
	if (formattedStrings != null) {
	    formattedStrings = new HashMap <Tuple, String> (formattedStrings);
	}
	if (tuples != null) {
	    List <Tuple> copy = new ArrayList <Tuple> (tuples.size());
	    copy.addAll(tuples);
	    tuples = copy;
	}
	// check constructor invariants only after defensive copy
	try {
	    validate();
	} catch (Exception e) {
	    InvalidObjectException x = new InvalidObjectException(
		    e.getMessage());
	    x.initCause(e);
	    throw x;
	}
    }

    /**
     * Gets a string representation of this instance useful for logging
     * and not intended for display.  The exact details of the
     * representation are unspecified and subject to change, but the
     * following format may be regarded as typical:
     * <pre><code>
     * class-name[property1 = value1, property2 = value2]
     * </code></pre>
     */
    public String
    toString()
    {
	StringBuilder buf = new StringBuilder();
	buf.append(PrintaRecord.class.getName());
	buf.append("[snaptime = ");
	buf.append(snaptime);
	buf.append(", aggregations = ");
	buf.append(aggregations);
	buf.append(", formattedStrings = ");
	buf.append(formattedStrings);
	buf.append(", tuples = ");
	buf.append(tuples);
	buf.append(", output = ");
	buf.append(getOutput());
	buf.append(']');
	return buf.toString();
    }
}
